UnityEditorのUndoで気をつけることファイナル
概要
ほぼすべての人類に無関係。十分にロックイン事案。
Undo自体の実装の話はこっち。
「UnityEditorのUndo/Redoシステムについて【解決編】【最新】のコピーそして完結」
http://sassembla.github.io/Public/2015:09:17%203-14-23/2015:09:17%203-14-23.html
今回は、なるべく面倒臭く無い方法で、管理される側のオブジェクトに辞書を使ったりする話。
端的にいうと最後、値をどう持つかで、値の保持にDictionaryを使うと辛い部分があった。
これはScriptableObjectを全開で使ってても、実際のデータの型に関連するので、例外なくつらい。
Dictionaryを使うと何が辛いのか
辞書 Dictionary をデータ構造に使うと、その部分がUndo効かない。
抽象的なコード書くと、
Undo.RecordObject(this, “Update Dictionary Value”);
myDict[“key”] = “val”;
みたいなことをしても、myDictの値がUndoできない。というかUndo履歴に乗らない。
[SerializeField] 振ってても、これは別の理由で効かないっぽい。
同様にList<string> とかに対して、
Undo.RecordObject(this, “Update List Value”);
myList.Add(“val”);
とかやると、これは効く。
違いを追っていく過程でわかったこと
いろいろ試していたら、次のようなことがわかった。完全にやっくしぇーびんぐ感があった。
・ISerializationCallbackReceiver とかはUndoとは関係無いっぽい
(Undoにはserialize的な特性は必要でも実務として特定のserialize処理が走ることは無いっぽくてISerializationCallbackReceiver実装しても呼ばれない
・Dict -> List + List に分解するとうまくいく
Dictionary<string, string> -> List<string> keys + List<string> values に分解したら、Undoは働いた
・調子に乗ってクラス化 + 型パラメータ化 -> 死ぬ
型パラメータが入るとだめくさい。
例えば下記のような構成は死ぬ。
[Serializable] public class SerializablePseudoDictionary<TKey, TValue> {
[SerializeField] private List<TKey> keys = new List<TKey>();
[SerializeField] private List<TValue> values = new List<TValue>();
. . .
こういうの定義できると楽だったんだが。
結論
List + GenericならUndo対象にできるので、Dictionary + Generics -> List<string> keys & List<string> values と分解できるようなクラス作ってやると瞬殺できる。
実際AssetGraphで使うことになった。 辞書っぽいもの作ってもUndoに対応できる。
抜粋。
using UnityEngine;
using System;
using System.Linq;
using System.Collections.Generic;
namespace AssetGraph {
/*
string key & string value only.
because generic dictionary class cannot undo.
write -> Add(k,v) -> new dict -> keys, values
read <- ReadonlyDict() <- new dict <- keys, values
*/
[Serializable] public class SerializablePseudoDictionary {
[SerializeField] private List<string> keys = new List<string>();
[SerializeField] private List<string> values = new List<string>();
public SerializablePseudoDictionary (Dictionary<string, string> baseDict) {
var dict = new Dictionary<string, string>(baseDict);
keys = dict.Keys.ToList();
values = dict.Values.ToList();
}
public void Add (string key, string val) {
var dict = new Dictionary<string, string>();
for (var i = 0; i < keys.Count; i++) {
var currentKey = keys[i];
var currentVal = values[i];
dict[currentKey] = currentVal;
}
// add or update parameter.
dict[key] = val;
keys = new List<string>(dict.Keys);
values = new List<string>(dict.Values);
}
public bool ContainsKey (string key) {
var dict = new Dictionary<string, string>();
for (var i = 0; i < keys.Count; i++) {
var currentKey = keys[i];
var currentVal = values[i];
dict[currentKey] = currentVal;
}
return dict.ContainsKey(key);
}
public Dictionary<string, string> ReadonlyDict () {
var dict = new Dictionary<string, string>();
if (keys == null) return dict;
for (var i = 0; i < keys.Count; i++) {
var key = keys[i];
var val = values[i];
dict[key] = val;
}
return dict;
}
}
お察しのとおり、read, writeに辞書化を挟んでるんで、使いやすいような気はあんましない。
Undo対象の数が少なかったのと、Undo動作自体を綺麗にまとめておくことができたので、実装と導入と改変は楽だった。
ただUnityとしか関係無い特殊な状況だ。
感想
試してないけどhashとかだとうまく動くんじゃ無いだろうか。
厄介なのは、Editor以外でもSerializable使っていろんなものをSerialize可能にすることができるんだけど、
これは結局ゲーム中でも使える手段だぜってところなんだけど、
・ListはOKでDictはダメ
・Serializableつけてもダメ
っていうのがある時点で、まあ、独自色強くなるんで、できうる限りゲームでDictのSerializeは使わないほうがいいだろうな、と思った。
ちなみにゲーム中( = 非Editor)の場合は、ISerializationCallbackReceiverまわりの関数が呼ばれるらしい。
Editorではそんなことなかったんで、まあ、はい。
無理にこういう[Serializable] + 独自Serializer実装するくらいなら、下記を実行するといいと思う。
a.Unityさんに殴り込んで「Serializableの有効範囲広げてや」って言う
b.[Serializable] を使ってDictionaryのSerializeが必要な状態を避ける、あるいはDictionary以外のものに対して[Serializable] を使う
c.[Serializable] を使わず自分で何かに変換する
b,cどちらの手段を選ぶにしても、Unityに「お前SerializeField効かない型があるの辛いぞ」っていう話をするのはアリだと思う。
自分からはします。